<?php

    /**
    * Mukei Development Framework
    *
    * Mukei is a development framework system created to provide developers
    * two main features: a plugin architecture which and expand the
    * functionality of the system and an event system which allows exansion of
    * existing plugins without modifing the original code.  The Mukei
    * development framework system can speed up development of new web
    * applications and reduce maintenance time of existing code.
    *
    * This file is part of Mukei.
    *
    * Mukei is free software; you can redistribute it and/or modify it under
    * the terms of the GNU General Public License as published by the Free
    * Software Foundation; either version 3 of the License, or (at your option)
    * any later version.
    *
    * Mukei is distributed in the hope that it will be useful, but WITHOUT ANY
    * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
    * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
    * details.
    *
    * You should have received a copy of the GNU General Public License
    * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    *
    * @package   Mukei
    * @version   0.2
    * @author    Kevin Herrera
    * @license   http://www.gnu.org/licenses/gpl.txt GNU GPL v3
    * @copyright Copyright (c) 2007 Kevin Herrera.
    */

    /**
    * Browser System Plugin
    *
    * This system plugin retrieves input from the browser's query string or
    * the command line.  The input is then parsed (if necessary) and then
    * cleaned using the system methods called clean() and strip().  The
    * resulting data (if any) will be stored in seperate arrays, a clean and
    * tainted array.  The different form submission methods are stored in
    * their own array that may be accessed by any plugin.  The GET method will
    * be stored in the $M->BRW->GET array (clean) and the $M->BRW->_GET array
    * (tainted).  The POST method will be stored in the $M->BRW->POST array
    * (clean) and the $M->BRW->_POST array (tainted).
    *
    * If the plugin does not care where the input is coming from, it may use
    * the ALL property ($M->BRW->ALL or $M->BRW->_ALL).  This property serves
    * the same purpose as GET and POST except it automatically detects which
    * one is in use.  The plugin may perform this check on its own by testing
    * the value of the M_INPUT constant.  The value can be compared against
    * M_GET for GET method and M_POST for POST method.  Cookies may also be
    * retrieved from the $M->BRW->COOKIE (clean) and $M->BRW->_COOKIE (tainted)
    * array properties.
    *
    * To change the value of the clean or tainted array, the system method
    * called update() must be used.  This method will automatically detect
    * what input method (GET or POST) is being used and update its clean and
    * tainted arrays.  To clear the value from a field, a call to update() must
    * be made and an empty string must be passed as the $value argument.
    *
    * This plugin also supports SEO (search engine optimization).  To
    * customize the request URI, the constants M_QUERY_SEP (field seperator,
    * default is /) and M_VALUE_SEP (field value seperator, default is ,).
    * These constants must be declared before this system plugin is loaded.
    * To enable SEO support, the constant M_SEO must be declared with the
    * boolean value of true.  By default this is how the URLs would look:
    *
    * Original:
    * http://www.yourdomain.com/index.php?act=SomePlugin&sub=value
    *
    * SEO Enabled (defaults):
    * http://www.yourdomain.com/act=Someplugin/sub=value
    *
    * If the system detects that the request URI is not empty but both the
    * GET and POST variables are, then SEO will automatically be enabled.
    *
    * @package Browser
    * @version 0.2
    * @final
    */
    final class Browser
    {
        /**#@+
        * @access public
        * @var    const
        */
        const AUTHOR   = "Kevin Herrera";
        const DESC     = "Collects GET/POST information and processes it.";
        const HANDLE   = "BRW";
        const NAME     = "Mukei Browser Agent";
        const REQUIRED = "0.2";
        const VERSION  = "0.2";
        const WEBSITE  = "http://www.mukei.org/";
        /**#@-*/

        /**#@+
        * @access private
        * @var    array
        */
        private $_C_COOKIE = array( );
        private $_C_GET    = array( );
        private $_C_POST   = array( );
        private $_T_COOKIE = array( );
        private $_T_GET    = array( );
        private $_T_POST   = array( );
        /**#@-*/

        /**
        * Class Constructor
        *
        * This system method collects environmental information and generates
        * the necessary data from the collected information.  First the method
        * detects which globals to use (_GET or HTTP_GET_VARS, etc...).
        * Second the method detects what input method was used (GET or POST).
        * Third, an attempt is made to define the input and input type
        * constants (M_INPUT, M_GET, and M_POST).  If the method is unable to
        * declare these constants, a fatal error will be triggered.  Fourth,
        * default constants are declared using the core function called
        * m_define().  Fifth, this method checks to see if SEO has been
        * enabled and performs the necessary modifications if it is enabled.
        * Last but not least, each of the gathered input globals will be
        * cleaned and stored in their own private array properties.
        *
        * @access public
        * @uses   m_define()
        * @uses   m_global()
        * @uses   Browser::_seo()
        * @uses   Browser::walk()
        * @return object
        */
        public function __construct ( )
        {
            // Get global.
            global $M;

            // Get cookie input.
            $this->_T_COOKIE = m_global( "_COOKIE",
                                         "HTTP_COOKIE_VAR" );

            // Get GET input.
            $this->_T_GET = m_global( "_GET",
                                      "HTTP_GET_VARS" );

            // Get POST input.
            $this->_T_POST = m_global( "_POST",
                                       "HTTP_POST_VARS" );

            // Determine input mode.
            $input = !empty( $this->_T_POST ) ? "post"
                                              : "get";

            // Define system constants.
            if ( !m_define( 'M_INPUT', $input ) ||
                 !m_define( 'M_GET',   "get"  ) ||
                 !m_define( 'M_POST',  "post" ) )
            {
                // Report fatal error.
                trigger_error( "Unable to define one or more required " .
                               "system plugin constants.",
                               E_USER_ERROR );
            }

            // Auto-enable SEO?
            if ( empty( $this->_T_GET  ) &&
                 empty( $this->_T_POST ) &&
                 !empty( $_SERVER['REQUEST_URI'] ) )
            {
                // Enable SEO.
                m_define( 'M_SEO', true );

                // Define constants.
                m_define( 'M_QUERY_SEP', "/"   );
                m_define( 'M_SEO',       false );
                m_define( 'M_VALUE_SEP', ","   );
            }

            // Define replaceable constants.
            m_define( 'M_QUERY_SEP', "&"   );
            m_define( 'M_SEO',       false );
            m_define( 'M_VALUE_SEP', "="   );

            // Clean input arrays.
            $this->_C_COOKIE = $this->walk( $this->_T_COOKIE );
            $this->_C_GET    = $this->walk( $this->_T_GET    );
            $this->_C_POST   = $this->walk( $this->_T_POST   );

            // SEO enabled?
            if ( M_SEO === true )
            {
                // Perform SEO changes.
                $this->_seo( );
            }
        }

        /**
        * Get Property Value
        *
        * This system overload method will detect what property is being
        * accessed and returns the appropriate data.  If the property ALL or
        * _ALL is requested, this method will check to see what input method
        * is currently being used.  If the M_INPUT value is M_GET, then the
        * value of the _C_GET or _T_GET array will be returned.  If GET or
        * _GET is requested, it will return the value of the _C_GET or _T_GET
        * array.  The same applies to POST, _POST, COOKIE, and _COOKIE.  If
        * the property name is not recognized by this method (via the switch
        * statement), the boolean value of false will be returned.
        *
        * @access public
        * @uses   m_arg()
        * @param  string $property Return property value.
        * @return mixed
        */
        public function __get ( $property )
        {
            // Check argument.
            m_arg( $property, M_STRING );

            // Initialize return.
            $return = false;

            // Switch on property name.
            switch ( $property )
            {
                // Is GET or POST?
                case "ALL":
                {
                    // Received GET?
                    if ( M_INPUT === M_GET )
                        $return = $this->_C_GET;

                    // Received POST?
                    else
                        $return = $this->_C_POST;

                    // Stop.
                    break;
                }

                // Is GET or POST tainted?
                case "_ALL":
                {
                    // Received GET?
                    if ( M_INPUT === M_GET )
                        $return = $this->_T_GET;

                    // Received POST?
                    else
                        $return = $this->_T_POST;

                    // Stop.
                    break;
                }

                // Is cookie?
                case "COOKIE":
                {
                    // Set return.
                    $return = $this->_C_COOKIE;

                    // Stop.
                    break;
                }

                // Is cookie tainted?
                case "_COOKIE":
                {
                    // Set return.
                    $return = $this->_C_COOKIE;

                    // Stop.
                    break;
                }

                // Is get?
                case "GET":
                {
                    // Set return.
                    $return = $this->_C_GET;

                    // Stop.
                    break;
                }

                // Is get tainted?
                case "_GET":
                {
                    // Set return.
                    $return = $this->_C_GET;

                    // Stop.
                    break;
                }

                // Is post?
                case "POST":
                {
                    // Set return.
                    $return = $this->_C_POST;

                    // Stop.
                    break;
                }

                // Is post tainted?
                case "_POST":
                {
                    // Set return.
                    $return = $this->_C_POST;

                    // Stop.
                    break;
                }
            }

            return( $return );
        }

        /**
        * Generate HTACCESS mod_rewrite File
        *
        * This system method generates a .htaccess file that will make URLs
        * search engine friendly.  This generated .htaccess file should be
        * used with SEO enabled.  To enable SEO, please declare the M_SEO
        * constant as true before the this system plugin is enabled.
        * Automatic detection of SEO URLs is performed but it should not be
        * solely relied upon.  This file should be saved manually so that
        * changes in file ownership will prevent it from being written to
        * by other plugins.  It is not suggested that the file generated be
        * writable by the system since this may pose as a security breach.
        *
        * @access public
        * @uses   m_global()
        * @uses   m_path()
        * @return boolean
        */
        public function _htaccess ( )
        {
            // Get global.
            $server = m_global( "_SERVER",
                                "HTTP_SERVER_VARS" );

            // Create relative URL.
            $relative = $server['PHP_SELF'];

            // Create file.
            $file  = "Options All -Indexes\n";
            $file .= "<IfModule mod_rewrite.c>\n";
            $file .= "RewriteEngine On\n";
            $file .= "RewriteBase /\n";
            $file .= "RewriteCond %{REQUEST_FILENAME} !-f\n";
            $file .= "RewriteCond %{REQUEST_FILENAME} !-d\n";
            $file .= "RewriteRule . $relative [L]\n";
            $file .= "</IfModule>\n";

            return( $file );
        }

        /**
        * Set Property Value
        *
        * This system method catches all attempts made at modifying non-
        * existant class properties.  Creating class properties is not allowed
        * within this class and manually updating COOKIE, GET, or POST data
        * is not allowed.  To update GET and POST data, please use the system
        * method called update().  This will handle your data and store it
        * in the appropriate arrays in tainted and non-tainted form.  To
        * modify or create cookies, please use the system method called
        * cookie().
        *
        * @access public
        * @see    Browser::cookie()
        * @see    Browser::update()
        * @param  string $property Property name.
        * @param  mixed  $value    Property value.
        * @return void
        */
        public function __set ( $property,
                                $value )
        {
            // Report warning.
            trigger_error( "Setting a property in the " . this::NAME      .
                           " system plugin is not allowed.  To set the "  .
                           "value of GET or POST, the Browser::update() " .
                           "method must be used.",
                           E_USER_WARNING );
        }

        /**
        * SEO Enabler
        *
        * This system method provides the Browser system plugin the ability
        * to support search engine friendly URLs, or SEO.  This method first
        * determines where this method can collect the request URI
        * information: the $_SERVER global or the $HTTP_SERVER_VARS global.
        * Next it removes the script's URI and leaves the actual query
        * information.  After the query information is extracted, it is then
        * parsed into an associative array using the M_QUERY_SEP and
        * M_VALUE_SEP constants.  If these constants were not defined by some
        * other plugin, the default values will be used (see the constructor).
        *
        * The associative array where the values are being stored is the array
        * for the current input mode.  If M_INPUT is currently M_GET, then
        * the values will be stored in GET and _GET.  The same applies to POST.
        * The values are stored in such a way that the original data from GET
        * and POST that did not use the search engine friendly URL format will
        * be kept if the SEO portion of the URL does not override it.
        *
        * @access private
        * @see    Browser::__construct()
        * @uses   m_global()
        * @uses   Browser::clean()
        * @uses   Browser::strip()
        * @return void
        */
        private function _seo ( )
        {
            // Get SERVER global.
            $server = m_global( "_SERVER",
                                "HTTP_SERVER_VARS" );

            // Get query.
            $query = $server['REQUEST_URI'];

            // Get self.
            $self = dirname( $_SERVER['PHP_SELF'] );

            // Create regex.
            $regex = M_SEO ? "/^\//"
                           : "/^\?/";

            // Split query.
            $query = explode( $self, $query );
            $query = array_pop( $query );

            // Remove remaining self.
            $query = preg_replace( $regex,
                                   "",
                                   $query );

            // Seperate fields.
            $pieces = explode( M_QUERY_SEP, $query );

            // Last piece empty?
            if ( empty( $pieces[ count( $pieces ) - 1] ) )
            {
                // Remove last piece.
                $pieces = array_slice( $pieces,
                                       0,
                                       count( $pieces ) - 1 );
            }

            // Walk through pieces.
            foreach ( $pieces as $piece )
            {
                // Initialize vars.
                $key   = "";
                $value = "";

                // Has seperator?
                if ( strstr( $piece, M_VALUE_SEP ) )
                {
                    // Seperate values.
                    list( $key, $value ) = explode( M_VALUE_SEP, $piece );
                }

                // No seperator?
                else
                {
                    // Use piece as key.
                    $key = $piece;
                }

                // Value is array?
                if ( is_array( $value ) )
                {
                    // Reconstruct value.
                    $value = join( M_VALUE_SEP, $value );
                }

                // NO value?
                if ( empty( $value ) )
                {
                    // Set default.
                    $value = "";
                }

                // Is GET mode?
                if ( M_INPUT === M_GET )
                {
                    // Save values.
                    $this->_T_GET[ $key ] = $value;

                    // Clean values.
                    $key   = $this->strip( $key   );
                    $value = $this->clean( $value );

                    // Save values.
                    $this->_C_GET[ $key ] = $value;
                }

                // Is POST mode?
                else
                {
                    // Save values.
                    $this->_T_POST[ $key ] = $value;

                    // Clean values.
                    $key   = $this->strip( $key   );
                    $value = $this->clean( $value );

                    // Save vlaues.
                    $this->_C_POST[ $key ] = $value;
                }
            }
        }

        /**
        * Clean String
        *
        * This system method converts all non-alphanumeric characters, except
        * the underscore, newline, and carriage return, to their HTML HEX
        * numbered entity equivelant.  By converting those characters, all
        * binary data and punctuation characters will be encoded and will
        * prevent hack attempts through this mode of input.  However, as extra
        * precaution, it is still recommended that you escape data where ever
        * needed, especially in sensitive situations such as SQL database
        * queries.
        *
        * @access public
        * @uses   m_arg()
        * @param  string $string String to clean.
        * @return string
        */
        public function clean ( $string = "" )
        {
            // Check argument.
            m_arg( $string, M_STRING, false );

            // Specify cleaner regex.
            $cleaner = "'&#' . ord( '\\0' ) . ';'";

            // Specify convert-back list.
            $revert = array(
                '/&#10;/' => "\n",
                '/&#13;/' => "\r",
                '/&#32;/' => " ",
            );

            // Replace characters.
            $string = preg_replace( "/\W/e",
                                    $cleaner,
                                    $string );

            // Revert back characters.
            $string = preg_replace( array_keys  ( $revert ),
                                    array_values( $revert ),
                                    $string );

            return( $string );
        }

        /**
        * Create/Update Cookie
        *
        * This system method will attempt to create a cookie with the provided
        * information.  The only two required arguments are the cookie's name
        * and value.  The rest of the arguments that are left blank will be
        * populated with default values that are guessed by this method.  If
        * headers are already sent or the cookie could not be created for
        * another reason, a warning will be triggered and the boolean value of
        * false will be returned.  The $http argument will only work in PHP
        * v5.2 or greater.  In versions of PHP earlier than 5.2, this argument
        * will be ignored.  If the cookie header was sent, the boolean value
        * of true will be returned.  This does not, however, mean that the
        * cookie was accepted by the browser.
        *
        * @access public
        * @uses   m_arg()
        * @param  string  $name             Cookie name.
        * @param  string  $value            Cookie value.
        * @param  integer $expires optional Expiration in Unix timestamp.
        * @param  string  $path    optional Cookie path.
        * @param  string  $domain  optional Cookie domain.
        * @param  boolean $secure  optional Is cookie for HTTPS only?
        * @param  boolean $http    optional HTTP only cookie?
        * @return boolean
        */
        public function cookie ( $name    = "",
                                 $value   = "",
                                 $expires = null,
                                 $path    = null,
                                 $domain  = null,
                                 $secure  = null,
                                 $http    = null )
        {
            // Check arguments.
            m_arg( $name,           M_STRING          );
            m_arg( $value,          M_STRING,   false );
            m_arg( $expires, array( M_INTEGER,
                                    M_NULL,
                                    M_STRING ), false );
            m_arg( $path,    array( M_NULL,
                                    M_STRING ), false );
            m_arg( $domain,  array( M_NULL,
                                    M_STRING ), false );
            m_arg( $secure,  array( M_NULL,
                                    M_BOOL   ), false );
            m_arg( $http,    array( M_NULL,
                                    M_BOOL   ), false );

            // Headers not sent?
            if ( !headers_sent( ) )
            {
                // Check expiration.
                if ( is_null( $expires ) )
                {
                    // Set default expiration.
                    $expires = 0;
                }

                // Check path.
                if ( is_null( $path ) )
                {
                    // Set default path.
                    $path = "";
                }

                // Check domain.
                if ( is_null( $domain ) )
                {
                    // Set default domain.
                    $domain = "";
                }

                // Check secure.
                if ( is_null( $secure ) )
                {
                    // Set default secure.
                    $secure = M_SECURE;
                }

                // Check http.
                if ( is_null( $http ) )
                {
                    // Set default http.
                    $http = false;
                }

                // Attempt to create cookie.
                $result = ( version_compare( phpversion( ), "5.2" ) >= 0 )
                        ? setcookie( $name,
                                     $value,
                                     $expires,
                                     $path,
                                     $domain,
                                     $secure,
                                     $http )
                        : setcookie( $name,
                                     $value,
                                     $expires,
                                     $path,
                                     $domain,
                                     $secure );

                // Cookie created?
                if ( $result === true )
                {
                    // Success.
                    return( true );
                }

                // Unable to create cookie?
                else
                {
                    // Report warning.
                    trigger_error( "Unable to create cookie.",
                                   E_USER_WARNING );
                }
            }

            // Headers sent?
            else
            {
                // Report warning.
                trigger_error( "Unable to create cookie.  HTTP headers have " .
                               "already been senet.",
                               E_USER_WARNING );
            }

            return( false );
        }

        /**
        * Strip String
        *
        * This system method removes all non-alphanumeric characters, except
        * the underscore, newline, and carriage return, from the string
        * provided.  No conversion is performed.  If the characters are still
        * needed, for HTML output for example, the system method called
        * clean() must be used instead.
        *
        * @access public
        * @see    Browser::clean()
        * @uses   m_arg()
        * @param  string $string String to strip.
        * @return string
        */
        public function strip ( $string = "" )
        {
            // Check argument.
            m_arg( $string, M_STRING, false );

            // Strip string.
            $string = preg_replace( "/\W/",
                                    "",
                                    $string );

            return( $string );
        }

        /**
        * Update Browser Query
        *
        * This system method will take the value and update the specified
        * field in the current input mode array (GET or POST).  The value will
        * be stored in it's tainted (_GET or _POST) and non-tainted (GET or
        * POST) form.  The key is stripped using the system method called
        * strip() and the value is cleaned using the system method called
        * clean().  To update a cookie's value, please see another system
        * method called cookie().
        *
        * @access public
        * @see    Browser::cookie()
        * @uses   m_arg()
        * @uses   Browser::clean()
        * @uses   Browser::strip()
        * @param  string  $key   Query field name.
        * @param  string  $value Query value.
        * @return boolean
        */
        public function update ( $field,
                                 $value )
        {
            // Check arguments.
            m_arg( $field, M_STRING        );
            m_arg( $value, M_STRING, false );

            // Get cleaned values.
            $c_field = $this->strip( $field );
            $c_value = $this->clean( $value );

            // GET mode?
            if ( M_INPUT === M_GET )
            {
                // Update GET values.
                $this->_T_GET[ $field ]   = $value;
                $this->_C_GET[ $c_field ] = $c_value;
            }

            // POST mode?
            else
            {
                // Update POST values.
                $this->_T_POST[ $field ]   = $value;
                $this->_C_POST[ $c_field ] = $c_value;
            }

            return( true );
        }

        /**
        * Create URL
        *
        * This system method generates an absolute URL using the system's
        * base URL constant (M_BASE_URL) and the associative array argument
        * provided, $query.  The keys in $query are used as the field names
        * while the values in $query are used as the field values.  The key
        * and value are both encoded before they are appended to the URL.  The
        * URL is generated to comply with the current SEO mode.  If SEO is
        * enabled, the URL's format will change.  Here are two examples of the
        * same data:
        *
        * SEO Enabled:
        * http://www.youdomain.com/act,Test/sub,Value
        *
        * SEO Disabled:
        * http://www.yourdomain.com/?act=Test&sub=Value
        *
        * @access public
        * @uses   m_arg()
        * @param  mixed  $query A string or associative array.
        * @return string
        */
        public function url ( $query = "" )
        {
            // Check arguments.
            m_arg( $query, array( M_STRING,
                                  M_ARRAY ), false );

            // Begin URL.
            $url = M_BASE_URL;

            // SEO not enabled?
            if ( M_SEO !== true )
                $url .= "?";

            // Is query an array?
            if ( is_array( $query ) )
            {
                // Initialize $fields.
                $fields = array( );

                // Walk through query.
                foreach ( $query as $key => $value )
                {
                    // Encode values.
                    $key   = urlencode( $key   );
                    $value = urlencode( $value );

                    // Compile and store.
                    $fields []= $key . M_VALUE_SEP . $value;
                }

                // Finish url.
                $url .= join( M_QUERY_SEP, $fields );
            }

            // Not an array?
            else
            {
                // Append query.
                $url .= $query;
            }

            return( $url );
        }

        /**
        * Clean Array
        *
        * This system method recursively cleans the array provided by using
        * the system methods clean() and strip().  If the value of an array
        * element is an array, this method will recurse by calling itself
        * using the value of the element.  All of the original key and value
        * data will be removed from the array returned.  If recursion needs to
        * be disabled when cleaning the array, the $recurse argument must be
        * set to false.  The array value will then be left untouched in the
        * returned array.  By default, recursion is enabled.
        *
        * @access public
        * @uses   m_arg()
        * @uses   Browser::clean()
        * @uses   Browser::strip()
        * @param  array   $array            Array to clean.
        * @param  boolean $recurse optional Enable/disable recursion?
        * @return array
        */
        public function walk ( $array,
                               $recurse = true )
        {
            // Check argument.
            m_arg( $array,   M_ARRAY, false );
            m_arg( $recurse, M_BOOL         );

            // Walk through array.
            foreach ( $array as $key => $value )
            {
                // Remove entry.
                unset( $array[ $key ] );

                // Strip key.
                $key = $this->strip( $key );

                // Is array?
                if ( is_array( $value ) )
                {
                    // Recursion enabled?
                    if ( $recurse === true )
                    {
                        // Recurse.
                        $value = $this->walk( $value );
                    }
                }

                // Is not an array?
                else
                {
                    // Clean value.
                    $value = $this->clean( $value );
                }

                // Save cleaned values.
                $array[ $key ] = $value;
            }

            return( $array );
        }
    }

?>